/**
 * Copyright (c) iwindplus Technologies Co., Ltd.2024-2030, All rights reserved.
 */

package com.iwindplus.gateway.server.filter;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.StopWatch;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ObjectUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.iwindplus.boot.domain.constant.CommonConstant.HeaderConstant;
import com.iwindplus.boot.domain.context.UserContextHolder;
import com.iwindplus.boot.domain.enums.BizCodeEnum;
import com.iwindplus.boot.domain.exception.BizException;
import com.iwindplus.boot.domain.vo.UserBaseVO;
import com.iwindplus.boot.util.CryptoUtil;
import com.iwindplus.boot.util.HttpsUtil;
import com.iwindplus.boot.util.JacksonUtil;
import com.iwindplus.boot.util.UrlUtil;
import com.iwindplus.boot.web.domain.property.FilterProperty;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant;
import com.iwindplus.gateway.server.domain.constant.GatewayConstant.ServerWebExchangeAttributeConstant;
import com.iwindplus.gateway.server.domain.property.GatewayProperty;
import com.iwindplus.gateway.server.util.GatewayUtil;
import com.iwindplus.mgt.api.power.ResourceApi;
import jakarta.annotation.Resource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

/**
 * 认证过滤器.
 *
 * @author zengdegui
 * @since 2020/4/15
 */
@Slf4j
@Component
public class AuthFilter implements GlobalFilter, Ordered {

    @Resource
    private GatewayProperty property;

    @Lazy
    @Resource
    private FilterProperty filterProperty;

    @Lazy
    @Resource
    private ResourceApi resourceApi;

    private static final Cache<Long, List<String>> PERMISSION_CACHE = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build();

    @Override
    public int getOrder() {
        return GatewayConstant.FilterConstant.FILTER_AUTH_ORDER;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 开关未启用跳过当前过滤器
        final Boolean whitedFlag = exchange.getAttribute(ServerWebExchangeAttributeConstant.WHITED_FLAG);
        if (Boolean.FALSE.equals(this.property.getAuth().getEnabled())
            || Boolean.TRUE.equals(whitedFlag)) {
            return chain.filter(exchange);
        }

        if (log.isInfoEnabled()) {
            // 统计执行时间任务
            StopWatch stopWatch = new StopWatch();
            stopWatch.start(AuthFilter.class.getSimpleName());

            final ServerWebExchange serverWebExchange = this.getServerWebExchange(exchange);
            return chain.filter(serverWebExchange)
                .doOnTerminate(() -> {
                    UserContextHolder.remove();
                    MDC.clear();

                    stopWatch.stop();
                    log.debug(stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
                });
        } else {
            final ServerWebExchange serverWebExchange = this.getServerWebExchange(exchange);
            return chain.filter(serverWebExchange)
                .doOnTerminate(() -> {
                    UserContextHolder.remove();
                    MDC.clear();
                });
        }
    }

    @NotNull
    private ServerWebExchange getServerWebExchange(ServerWebExchange exchange) {
        final String authorization = GatewayUtil.getAuthorization(exchange);
        if (CharSequenceUtil.isBlank(authorization)) {
            throw new BizException(BizCodeEnum.TOKEN_NOT_EXIST);
        }
        // 校验token
        ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
        final UserBaseVO userInfo = this.buildUserInfo(authorization, builder);
        // 校验API权限
        if (Boolean.TRUE.equals(this.property.getAuth().getEnabledApiPermission())) {
            String requestPath = exchange.getRequest().getPath().value();
            // 忽略鉴权白名单
            final boolean whitedFlag = this.hasIgnoredWhitedFlag(requestPath);
            if (Boolean.FALSE.equals(whitedFlag)) {
                this.checkApiPermission(requestPath, userInfo);
            }
        }
        final ServerWebExchange serverWebExchange = exchange.mutate().request(builder.build()).build();
        return serverWebExchange;
    }

    private UserBaseVO buildUserInfo(String authorization, ServerHttpRequest.Builder builder) {
        final UserBaseVO userInfo = HttpsUtil.getUserInfo(authorization);
        if (ObjectUtil.isEmpty(userInfo)) {
            throw new BizException(BizCodeEnum.INVALID_ACCESS_TOKEN);
        }

        final String jsonStr = JacksonUtil.toJsonStr(userInfo);
        final String userInfoStr = CryptoUtil.encrypt(jsonStr, this.filterProperty.getCrypto());
        MDC.put(HeaderConstant.USER_INFO, userInfoStr);
        builder.header(HeaderConstant.USER_INFO, userInfoStr);
        UserContextHolder.setContext(userInfo);
        return userInfo;
    }

    private boolean hasIgnoredWhitedFlag(String requestPath) {
        List<String> ignoredPatterns = property.getAuth().getIgnoredApi();
        if (CollUtil.isNotEmpty(ignoredPatterns)) {
            List<String> whiteList = ignoredPatterns.stream().distinct().collect(Collectors.toCollection(ArrayList::new));
            return UrlUtil.isMatchLike(whiteList, requestPath);
        }
        return false;
    }

    private void checkApiPermission(String requestPath, UserBaseVO userInfo) {
        Long userId = userInfo.getUserId();
        List<String> apis = PERMISSION_CACHE.getIfPresent(userId);
        if (ObjectUtil.isEmpty(apis)) {
            this.loadApiPermission(userInfo);
        }
        if (ObjectUtil.isEmpty(apis)) {
            return;
        }

        if (Boolean.FALSE.equals(UrlUtil.isMatch(apis, requestPath))) {
            throw new BizException(BizCodeEnum.NO_API_PERMISSION);
        }
    }

    private void loadApiPermission(UserBaseVO userInfo) {
        Set<String> result = new HashSet<>(16);
        // 通用都有权限的API
        final List<String> generalApi = property.getAuth().getGeneralApi();
        if (CollUtil.isNotEmpty(generalApi)) {
            result.addAll(generalApi);
        }

        Mono.justOrEmpty(this.resourceApi.listApiCheckedByUserId(userInfo.getOrgId(), userInfo.getUserId()))
            .retryWhen(Retry.backoff(this.property.getRemoteRetryCount(), this.property.getRemoteRetryPeriod()))
            .subscribe(data -> {
                if (CollUtil.isNotEmpty(data.getBizData())) {
                    result.addAll(data.getBizData());
                }
            });
        if (ObjectUtil.isNotEmpty(result)) {
            PERMISSION_CACHE.put(userInfo.getUserId(), List.copyOf(result));
        }
    }

}
